/*
 ------------------------------------------------------------------------------
 Copyright (C) 2006-2007 Team Blur.

 This file is part of the Quantum Engine source code.

 The Quantum Engine source code is free software; you can redistribute it and/or
 modify it under the terms of the GNU General Public License as published by
 the Free Software Foundation; either version 2 of the License, or (at your
 option) any later version.

 The Quantum Engine source code is distributed in the hope that it will be useful,
 but WITHOUT ANY WARRANTY; without even the implied warranty of MERCHANTABILITY
 or FITNESS FOR A PARTICULAR PURPOSE.  See the GNU General Public License for
 more details.

 You should have received a copy of the GNU General Public License along with
 the Quantum Engine source code; if not, write to the Free Software Foundation, Inc.,
 51 Franklin Street, Fifth Floor, Boston, MA  02110-1301, USA.
 ------------------------------------------------------------------------------
*/


#include "s_local.h"


#define SOUNDS_HASH_SIZE				MAX_SOUNDS / 4

typedef struct {
	byte *				data;
	byte *				dataPtr;
	byte *				dataEnd;

	byte *				lastChunk;
	int					chunkLen;
} wavFile_t;

static sound_t *		s_soundsHashTable[SOUNDS_HASH_SIZE];
static sound_t *		s_sounds[MAX_SOUNDS];
static int				s_numSounds;


/*
 ==============================================================================

 WAV LOADING

 ==============================================================================
*/


/*
 ==================
 S_FindChunk
 ==================
*/
static void S_FindChunk (wavFile_t *file, const char *name){

	file->lastChunk = file->data;

	while (1){
		file->dataPtr = file->lastChunk;

		if (file->dataPtr >= file->dataEnd){
			// Didn't find the chunk
			file->dataPtr = NULL;
			return;
		}

		file->dataPtr += 4;

		file->chunkLen = file->dataPtr[0] | (file->dataPtr[1] << 8) | (file->dataPtr[2] << 16) | (file->dataPtr[3] << 24);
		if (file->chunkLen < 0){
			// Invalid chunk
			file->dataPtr = NULL;
			return;
		}

		file->dataPtr -= 4;
		file->lastChunk = file->dataPtr + 8 + ALIGN(file->chunkLen, 2);

		if (!Str_CompareChars((char *)file->dataPtr, name, 4))
			return;		// Found it
	}
}


/*
 ==================
 S_LoadWAV
 ==================
*/
static bool S_LoadWAV (const char *name, byte **wav, int *rate, int *samples){

	wavFile_t	file;
	wavHeader_t	header;
	byte		*data;
	byte		*in, *out;
	int			i, size;

	// Load the file
	size = FS_ReadFile(name, (void **)&data);
	if (!data)
		return false;

	file.data = data;
	file.dataEnd = data + size;

	// Find the 'RIFF/WAVE' chunks
	S_FindChunk(&file, "RIFF");

	if (!(file.dataPtr && !Str_CompareChars((char *)(file.dataPtr + 8), "WAVE", 4)))
		Com_Error(false, "S_LoadWAV: missing 'RIFF/WAVE' chunks (%s)", name);

	file.data = data + 12;
	file.dataEnd = data + size;

	// Find the 'fmt ' chunk
	S_FindChunk(&file, "fmt ");

	if (!file.dataPtr)
		Com_Error(false, "S_LoadWAV: missing 'fmt ' chunk (%s)", name);

	file.dataPtr += 8;

	// Parse the WAV header
	header.format = file.dataPtr[0] | (file.dataPtr[1] << 8);
	header.channels = file.dataPtr[2] | (file.dataPtr[3] << 8);
	header.samplesPerSec = file.dataPtr[4] | (file.dataPtr[5] << 8) | (file.dataPtr[6] << 16) | (file.dataPtr[7] << 24);
	header.avgBytesPerSec = file.dataPtr[8] | (file.dataPtr[9] << 8) | (file.dataPtr[10] << 16) | (file.dataPtr[11] << 24);
	header.blockAlign = file.dataPtr[12] | (file.dataPtr[13] << 8);
	header.bitsPerSample = file.dataPtr[14] | (file.dataPtr[15] << 8);

	file.dataPtr += 16;

	if (header.format != 1)
		Com_Error(false, "S_LoadWAV: only Microsoft PCM sound format supported (%s)", name);

	if (header.channels != 1)
		Com_Error(false, "S_LoadWAV: only mono sounds supported (%s)", name);

	if (header.bitsPerSample != 16)
		Com_Error(false, "S_LoadWAV: only 16 bit sounds supported (%s)", name);

	// Find the 'data' chunk
	S_FindChunk(&file, "data");

	if (!file.dataPtr)
		Com_Error(false, "S_LoadWAV: missing 'data' chunk (%s)", name);

	file.dataPtr += 4;

	size = (file.dataPtr[0] | (file.dataPtr[1] << 8) | (file.dataPtr[2] << 16) | (file.dataPtr[3] << 24));
	if (size <= 0)
		Com_Error(false, "S_LoadWAV: bad sound data size (%i) (%s)", size, name);

	// Read the sound samples
	in = file.dataPtr + 4;

	*wav = out = (byte *)Mem_Alloc(size, TAG_TEMP);

	*rate = header.samplesPerSec;
	*samples = size / 2;

	for (i = 0; i < size / 2; i++)
		((short *)out)[i] = LittleShort(((short *)in)[i]);

	// Free file data
	FS_FreeFile(data);

	return true;
}


/*
 ==============================================================================

 SOUND LOADING

 ==============================================================================
*/


/*
 ==================
 S_ResampleSound
 ==================
*/
static void S_ResampleSound (byte *wav, sound_t *sound){

	float	stepScale;
	uint	frac, fracStep;
	int		sample;
	int		i;

	stepScale = (float)sound->rate / s_maxSampleRate->integerValue;

	sound->rate = s_maxSampleRate->integerValue;
	sound->samples /= stepScale;

	frac = 0;
	fracStep = stepScale * 256;

	for (i = 0; i < sound->samples; i++){
		sample = (frac >> 8);
		frac += fracStep;

		((short *)wav)[i] = ((short *)wav)[sample];
	}
}

/*
 ==================
 S_UploadSound
 ==================
*/
static void S_UploadSound (byte *wav, sound_t *sound){

	// Resample the sound if desired
	if (s_maxSampleRate->integerValue > 0 && !(sound->flags & SF_NORESAMPLE)){
		if (s_maxSampleRate->integerValue < sound->rate)
			S_ResampleSound(wav, sound);
	}

	// Calculate sound size
	sound->size = sound->samples * 2;

	// Copy the sound
	sound->data = (byte *)Mem_Alloc(sound->size, TAG_SOUND);
	memcpy(sound->data, wav, sound->size);

	// Upload the sound
	qalGenBuffers(1, &sound->bufferId);
	qalBufferData(sound->bufferId, AL_FORMAT_MONO16, sound->data, sound->size, sound->rate);
}

/*
 ==================
 S_LoadSound
 ==================
*/
static sound_t *S_LoadSound (const char *name, byte *wav, int rate, int samples, int flags){

	sound_t	*sound;
	uint	hashKey;

	if (s_numSounds == MAX_SOUNDS)
		Com_Error(false, "S_LoadSound: MAX_SOUNDS hit");

	s_sounds[s_numSounds++] = sound = (sound_t *)Mem_Alloc(sizeof(sound_t), TAG_SOUND);

	// Fill it in
	Str_Copy(sound->name, name, sizeof(sound->name));
	sound->flags = flags;
	sound->rate = rate;
	sound->samples = samples;
	sound->length = (samples * 1000) / rate;

	S_UploadSound(wav, sound);

	// Add to hash table
	hashKey = Str_HashKey(sound->name, SOUNDS_HASH_SIZE, false);

	sound->nextHash = s_soundsHashTable[hashKey];
	s_soundsHashTable[hashKey] = sound;

	return sound;
}

/*
 ==================
 S_FindSound
 ==================
*/
sound_t *S_FindSound (const char *name, int flags){

	sound_t	*sound;
	byte	*wav;
	int		rate, samples;
	uint	hashKey;

	// See if already loaded
	hashKey = Str_HashKey(name, SOUNDS_HASH_SIZE, false);

	for (sound = s_soundsHashTable[hashKey]; sound; sound = sound->nextHash){
		if (!Str_ICompare(sound->name, name)){
			if (sound->flags & SF_INTERNAL)
				return sound;

			if (sound->flags != flags)
				Com_Printf(S_COLOR_YELLOW "WARNING: reused sound '%s' with mixed flags parameter\n", name);

			return sound;
		}
	}

	// Load it from disk
	if (!S_LoadWAV(name, &wav, &rate, &samples))
		return NULL;

	// Load the sound
	sound = S_LoadSound(name, wav, rate, samples, flags);

	Mem_Free(wav);

	return sound;
}


// ============================================================================


/*
 ==================
 S_ListSounds_f
 ==================
*/
static void S_ListSounds_f (void){

	sound_t	*sound;
	int		bytes = 0;
	int		i;

	Com_Printf("\n");
	Com_Printf("      -hz-- -samples -size- length -name--------\n");

	for (i = 0; i < s_numSounds; i++){
		sound = s_sounds[i];

		bytes += sound->size;

		Com_Printf("%4i: ", i);

		Com_Printf("%5i %8i ", sound->rate, sound->samples);

		Com_Printf("%5ik ", sound->size >> 10);

		Com_Printf("%5.2fs ", MS2SEC(sound->length));

		Com_Printf("%s\n", sound->name);
	}

	Com_Printf("------------------------------------------------\n");
	Com_Printf("%i total sounds\n", s_numSounds);
	Com_Printf("%.2f MB of sound data\n", bytes * (1.0f/1048576));
	Com_Printf("\n");
}

/*
 ==================
 S_CreateBuiltInSounds
 ==================
*/
static void S_CreateBuiltInSounds (void){

	byte	wav[22050];
	int		i;

	// Default sound
	for (i = 0; i < 11025; i++)
		((short *)wav)[i] = (short)(M_Sin((float)i * 0.1f) * 20000.0f);

	snd.defaultSound = S_LoadSound("_default", wav, 22050, 11025, SF_INTERNAL | SF_NORESAMPLE);
}

/*
 ==================
 S_InitSounds
 ==================
*/
void S_InitSounds (void){

	// Register our commands
	Cmd_AddCommand("listSounds", S_ListSounds_f, "Lists loaded sounds");

	// Create built-in sounds
	S_CreateBuiltInSounds();
}

/*
 ==================
 S_ShutdownSounds
 ==================
*/
void S_ShutdownSounds (void){

	sound_t	*sound;
	int		i;

	// Unregister our commands
	Cmd_RemoveCommand("listSounds");

	// Delete all sounds
	for (i = 0; i < s_numSounds; i++){
		sound = s_sounds[i];

		qalDeleteBuffers(1, &sound->bufferId);
	}

	// Clear sound list
	memset(s_soundsHashTable, 0, sizeof(s_soundsHashTable));
	memset(s_sounds, 0, sizeof(s_sounds));

	s_numSounds = 0;
}
